﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;

namespace OpenLS.Spreadsheet
{
    internal class CellsWindow
    {
        private readonly Dictionary<CellClusterKey, CellCluster> _data = new Dictionary<CellClusterKey, CellCluster>();
        private readonly Worksheet _worksheet;

        private int _bottom;
        private bool _disposed;
        private int _left;
        private int _right;
        private int _top;

        public CellsWindow(Worksheet worksheet, RangeAddress address)
        {
            Contract.Requires(worksheet != null);
            _worksheet = worksheet;
            HookupEvents();
            foreach (Cell cell in (new MultiCellsRange(worksheet, address)).GetNonEmptyCells())
                AddCell(cell);
            _top = address.TopLeft.Row;
            _bottom = address.BottomRight.Row;
            _left = address.TopLeft.Column;
            _right = address.BottomRight.Column;
        }

        private RangeAddress RangeAddress
        {
            get { return new RangeAddress(new CellAddress(Top, Left), new CellAddress(Bottom, Right)); }
        }

        protected Worksheet Worksheet
        {
            get { return _worksheet; }
        }

        private int Right
        {
            get { return _right; }
        }

        internal int Left
        {
            get { return _left; }
        }

        public int Bottom
        {
            get { return _bottom; }
        }

        public int Top
        {
            get { return _top; }
        }

        public event EventHandler<ListEventArgs<CellClusterKey>> ClustersAdded;
        public event EventHandler<ListEventArgs<CellClusterKey>> ClustersRemoved;
        public event EventHandler<ListEventArgs<Cell>> CellsAdded;
        public event EventHandler<ListEventArgs<Cell>> CellsRemoved;

        private void HookupEvents()
        {
            _worksheet.CellAdded += WorksheetCellAdded;
            _worksheet.CellsRemoved += WorksheetCellsRemoved;
        }

        ~CellsWindow()
        {
            Dispose();
        }

        public void Dispose()
        {
            if (_disposed)
                return;
            _worksheet.CellAdded -= WorksheetCellAdded;
            _worksheet.CellsRemoved -= WorksheetCellsRemoved;

            _disposed = true;
        }

        private void WorksheetCellsRemoved(object sender, CellRegionEventArgs e)
        {
            Contract.Assert(Enumerable.Count(Enumerable.Distinct(e.Region.GetCells(RangeAddress))) ==
                            Enumerable.Count(e.Region.GetCells(RangeAddress)));

            List<Cell> cells = new List<Cell>();
            RemoveNonEmptyClusters(Enumerable.Where(_data.Keys, k => e.Region.GetClusterImpact(k) == ClusterImpact.Full));
            foreach (Cell cell in e.Region.GetCells(RangeAddress))
            {
                cells.Add(cell);
                RemoveCell(cell);
            }
            if (CellsRemoved != null && cells.Count != 0)
                CellsRemoved(this, new ListEventArgs<Cell>(cells));
        }

        private void RemoveNonEmptyClusters(IEnumerable<CellClusterKey> clusterKeys)
        {
            List<CellClusterKey> keys = Enumerable.ToList(clusterKeys);
            if (keys.Count == 0)
                return;
            List<Cell> cells = null;
            if (CellsRemoved != null)
                cells = Enumerable.ToList(Enumerable.SelectMany(keys, k => _data[k]));
            foreach (CellClusterKey key in Enumerable.ToList(keys))
                _data.Remove(key);

            if (CellsRemoved != null && cells.Count > 0)
                CellsRemoved(this, new ListEventArgs<Cell>(cells));
            if (ClustersRemoved != null)
                ClustersRemoved(this, new ListEventArgs<CellClusterKey>(keys));
        }

        private void RemoveCell(Cell cell)
        {
            foreach (CellClusterKey key in CellClusterKey.GetKeys(cell))
            {
                CellCluster cluster;
                if (_data.TryGetValue(key, out cluster))
                {
                    cluster.Remove(cell);
                    if (cluster.Count == 0)
                        RemoveEmptyClusters(new List<CellClusterKey> {key});
                }
            }
        }

        private void WorksheetCellAdded(object sender, CellEventArgs e)
        {
            if (RangeAddress.Contains(e.Cell.Address))
            {
                AddCell(e.Cell);
            }
        }

        internal void VerticalChange(int newTop, int newBottom)
        {
            Contract.Requires(newTop <= Worksheet.MaxRowIndex);
            Contract.Requires(newBottom <= Worksheet.MaxRowIndex);
            Contract.Requires(newTop >= 0);
            Contract.Requires(newBottom >= newTop);
            Contract.Ensures(Top == newTop);
            Contract.Ensures(Bottom == newBottom);
            if (newTop == Top && newBottom == Bottom)
                return;
            if (newTop > Bottom || newBottom < Top)
            {
                Clear();
                Build(new RangeAddress(new CellAddress(newTop, Left), new CellAddress(newBottom, Right)));
            }
            else
            {
                if (newTop < Top)
                    AddCells(new RangeAddress(new CellAddress(newTop, Left), new CellAddress(Top - 1, Right)));
                else if (newTop > Top)
                    RemoveCells(new RangeAddress(new CellAddress(Top, Left), new CellAddress(newTop - 1, Right)));
                if (newBottom > Bottom)
                    AddCells(new RangeAddress(new CellAddress(Bottom + 1, Left), new CellAddress(newBottom, Right)));
                else if (newBottom < Bottom)
                    RemoveCells(new RangeAddress(new CellAddress(newBottom + 1, Left), new CellAddress(Bottom, Right)));
            }
            if (VerticalChanged != null)
                VerticalChanged(this, new BoundsChangedEventArgs(_top, newTop, _bottom, newBottom));

            _top = newTop;
            _bottom = newBottom;
        }

        internal void HorizontalChange(int newLeft, int newRight)
        {
            Contract.Requires(newLeft >= 0);
            Contract.Requires(newRight >= newLeft);
            Contract.Ensures(Left == newLeft);
            Contract.Ensures(Right == newRight);
            if (newLeft == Left && newRight == Right)
                return;
            if (newLeft > Right || newRight < Left)
            {
                Clear();
                Build(new RangeAddress(new CellAddress(Top, newLeft), new CellAddress(Bottom, newRight)));
            }
            else
            {
                if (newLeft < Left)
                    AddCells(new RangeAddress(new CellAddress(Top, newLeft), new CellAddress(Bottom, Left - 1)));
                else if (newLeft > Left)
                    RemoveCells(new RangeAddress(new CellAddress(Top, Left), new CellAddress(Bottom, newLeft - 1)));
                if (newRight > Right)
                    AddCells(new RangeAddress(new CellAddress(Top, Right + 1), new CellAddress(Bottom, newRight)));
                else if (newRight < Right)
                    RemoveCells(new RangeAddress(new CellAddress(Top, newRight + 1), new CellAddress(Bottom, Right)));
            }
            if (HorizontalChanged != null)
                HorizontalChanged(this, new BoundsChangedEventArgs(_left, newLeft, _right, newRight));
            _left = newLeft;
            _right = newRight;
        }

        public class BoundsChangedEventArgs : EventArgs
        {
            public readonly int NewFirst;
            public readonly int NewLast;
            public readonly int OldFirst;
            public readonly int OldLast;

            public BoundsChangedEventArgs(int oldFirst, int newFirst, int oldLast, int newLast)
            {
                OldFirst = oldFirst;
                NewFirst = newFirst;
                OldLast = oldLast;
                NewLast = newLast;
            }
        }

        public event EventHandler<BoundsChangedEventArgs> HorizontalChanged;
        public event EventHandler<BoundsChangedEventArgs> VerticalChanged;

        private IEnumerable<Cell> GetCells(CellClusterKey cellClusterKey)
        {
            return _data[cellClusterKey];
        }

        private void RemoveCells(RangeAddress rangeAddress)
        {
            var toRemove = new List<CellClusterKey>();
            var cells = new List<Cell>();
            Contract.Assert(Enumerable.Count(_worksheet.GetCells(rangeAddress)) ==
                            Enumerable.Count(Enumerable.Distinct(_worksheet.GetCells(rangeAddress))));
            foreach (Cell cell in _worksheet.GetCells(rangeAddress))
            {
                foreach (CellClusterKey key in CellClusterKey.GetKeys(cell))
                {
                    CellCluster cluster;
                    if (_data.TryGetValue(key, out cluster))
                    {
                        cluster.Remove(cell);
                        if (cluster.Count == 0)
                            toRemove.Add(key);
                    }
                    else
                    {
                       // Contract.Assert(false); //\\sure?
                    }
                }
                cells.Add(cell);
            }
            if (CellsRemoved != null)
            {
                List<Cell> cells2 = Enumerable.ToList(CellHelper.DistinctCells(cells));
                if (cells2.Count > 0)
                    CellsRemoved(this, new ListEventArgs<Cell>(cells2));
            }

            RemoveEmptyClusters(toRemove);
        }

        private void RemoveEmptyClusters(List<CellClusterKey> toRemove)
        {
            Contract.Assert(toRemove != null);
            foreach (CellClusterKey key in toRemove)
            {
                Contract.Assert(_data[key].Count == 0);
                _data.Remove(key);
            }
            if (ClustersRemoved != null && toRemove.Count > 0)
                ClustersRemoved(this, new ListEventArgs<CellClusterKey>(toRemove));
        }

        private void AddCells(RangeAddress rangeAddress)
        {
            var list = new List<CellClusterKey>();
            var cells = new List<Cell>();
            Contract.Assert(
                (Enumerable.Count(this.Worksheet.GetCells(rangeAddress)) ==
                                  Enumerable.Count(Enumerable.Distinct(this.Worksheet.GetCells(rangeAddress)))));
            foreach (Cell cell in Worksheet.GetCells(rangeAddress))
            {
                Contract.Assert(cell.GetRangeAddress().Intersects(rangeAddress));
                bool hasSpan = cell.HasSpan;
                foreach (CellClusterKey key in CellClusterKey.GetKeys(cell))
                {
                    var cluster = EnsureKey(key, list);
                    if (!hasSpan || !cluster.Contains(cell))
                        cluster.Add(cell);
                    cells.Add(cell);
                }
            }
            if (list.Count > 0 && ClustersAdded != null)
                ClustersAdded(this, new ListEventArgs<CellClusterKey>(list));
            if (cells.Count > 0 && CellsAdded != null)
                CellsAdded(this, new ListEventArgs<Cell>(Enumerable.ToList(CellHelper.DistinctCells(cells))));
        }

        private IEnumerable<CellClusterKey> GetKeys(RangeAddress rangeAddress)
        {
            int minRow = rangeAddress.TopLeft.Row/CellClusterKey.Size*CellClusterKey.Size;
            int maxRow = rangeAddress.BottomRight.Row/CellClusterKey.Size*CellClusterKey.Size;
            int minColumn = rangeAddress.TopLeft.Column/CellClusterKey.Size*CellClusterKey.Size;
            int maxColumn = rangeAddress.BottomRight.Column/CellClusterKey.Size*CellClusterKey.Size;
            for (int row = minRow; row <= maxRow; row += CellClusterKey.Size)
                for (int column = minColumn; column <= maxColumn; column += CellClusterKey.Size)
                    yield return new CellClusterKey(row, column);
        }


        private void Build(RangeAddress rangeAddress)
        {
            AddCells(rangeAddress);
        }

        private void Clear()
        {
            Contract.Ensures(Enumerable.Count(GetCells()) == 0);
            _data.Clear();
            if (Cleared != null)
                Cleared(this, new EventArgs());
        }

        internal event EventHandler Cleared;


        private CellCluster EnsureKey(CellClusterKey key, List<CellClusterKey> addedKeys)
        {
            Contract.Requires(addedKeys != null);
            Contract.Ensures(Contract.Result<CellCluster>() != null);
            CellCluster result;
            if (!_data.TryGetValue(key, out result))
            {
                result = new CellCluster(key);
                _data.Add(key, result);
                addedKeys.Add(key);
            }
            return result;
        }

        private void AddCell(Cell cell)
        {
            Contract.Requires(cell != null);
            Contract.Requires(cell.Worksheet == Worksheet);
            var l = new List<CellClusterKey>();
            foreach (CellClusterKey key in CellClusterKey.GetKeys(cell))
            {
                EnsureKey(key, l).Add(cell);
            }
            if (ClustersAdded != null && l.Count > 0)
                ClustersAdded(this, new ListEventArgs<CellClusterKey>(l));
            if (CellsAdded != null)
                CellsAdded(this, new ListEventArgs<Cell>(new List<Cell> {cell}));
#if DEBUG
            CheckConsistent();
#endif
        }

#if DEBUG
        private void CheckConsistent()
        {
            foreach (Cell cell in GetCells())
            {
                foreach (CellClusterKey key in CellClusterKey.GetKeys(cell))
                {
                    Contract.Assert(_data.ContainsKey(key));
                    Contract.Assert(_data[key].Contains(cell));
                }
            }
        }

#endif

        public IEnumerable<Cell> GetCells()
        {
            return CellHelper.DistinctCells(Enumerable.SelectMany(_data.Values, v => v));
        }

        #region Nested type: ClusterImpact

        #endregion

        public bool Intersects(CellClusterKey cellClusterKey)
        {
            var other = cellClusterKey.RangeAddress;
            var thisRangeAddress = new RangeAddress(new CellAddress(Top, Left), new CellAddress(Bottom, Right));
            return thisRangeAddress.Intersects(other);

        }
    }
}